JSON.stringify는 순환참조 객체를 지원하지 못하고 예외를 던진다. feat.axios

문제현상

  1. axios에서 예외가 발생함
  2. 해당 예외 객체를 stringifyError() 메서드에 전달하여 로그로 출력하려고 시도함
  3. 하지만 axios의 에러 객체는 내부적으로 순환참조(circular reference) 를 포함하고 있음
  4. JSON.stringify()는 기본적으로 순환참조가 있는 객체를 직렬화할 수 없기 때문에 예외(TypeError: Converting circular structure to JSON)를 발생시킴
 private stringifyError(error: unknown): string {
    if (error instanceof Error) {
      const details: Record<string, unknown> = {
        name: error.name,
        message: error.message,
        stack: error.stack,
      };
      for (const key of Object.getOwnPropertyNames(error)) {
        if (!(key in details)) {
          // eslint-disable-next-line @typescript-eslint/no-explicit-any
          (details as any)[key] = (error as any)[key];
        }
      }
      return JSON.stringify(details); // <-- 💀
    }
    try {
      return JSON.stringify(error);
    } catch {
      return String(error);
    }
  }

원인분석

written by Chat GPT 4o


해결 방법

1. json-stringify-safe 사용 (추천)

npm install json-stringify-safe
import stringifySafe from 'json-stringify-safe';

return stringifySafe(details);

json-stringify-safe는 순환참조가 감지되면 [Circular]로 대체하여 문자열을 생성함


2. 수동 순환 제거

config, request, response 등을 제거하거나 최소한의 필드만 추출하여 별도 처리

const cleanedError = {
  name: error.name,
  message: error.message,
  stack: error.stack,
  isAxiosError: (error as any).isAxiosError,
  code: (error as any).code,
  url: (error as any)?.config?.url,
  method: (error as any)?.config?.method,
  status: (error as any)?.response?.status,
};
return JSON.stringify(cleanedError);

3. 에러 객체 로깅 전 fallback 처리

try {
  return JSON.stringify(details);
} catch (e) {
  return `Unserializable error object: ${String(error)}`;
}

요약

NodeJS 환경에서 util.inspect 함수를 써도 된다.

역직렬화는 안되지만 콘솔 출력시 객체를 문자열로 변환할때 쓰는 좋은 함수이다.

참조: https://nodejs.org/api/util.html#utilinspectobject-options

위에서 걱정했던 순환참조도 알아서 [Circular]로 감싸준다

const { inspect } = require('node:util');

const obj = {};
obj.a = [obj];
obj.b = {};
obj.b.inner = obj.b;
obj.b.obj = obj;

console.log(inspect(obj));
// <ref *1> {
//   a: [ [Circular *1] ],
//   b: <ref *2> { inner: [Circular *2], obj: [Circular *1] }
// }